﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using mshtml;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Configuration;

namespace jTest
{
    /// <summary>
    /// Use jQuery to scrape an html document.
    /// </summary>
    public class jScrape
    {
    // Constants
        private const string ResultJavascriptRelitivePath = @"\HtmlHelpers\Results.js";
        private const string ResultStyleSheetRelitivePath = @"\HtmlHelpers\ResultStyles.css";
        private const string HtmlHelperKey = "jTest.HtmlHelpers";

    // Constructor
        private jScrape()
        { }

    // Properties
        /// <summary>
        /// The jQuery selector mapping used to generate the result listing.
        /// </summary>
        public IEnumerable<String> SelectorMappings { get; set; }
         
        /// <summary>
        /// The mapping used to generate the result listing.
        /// </summary>
        public IEnumerable<String> Mappings { get; set; }
        
        /// <summary>
        /// The results from the map, the key is the mapping.
        /// </summary>
        public Dictionary<String, IEnumerable<ScrapeResult>> Results 
        { 
            get; 
            private set; 
        }

        /// <summary>
        /// If a 404 error is thrown let me know.
        /// </summary>
        public int PageResponseType { get; set; }

        /// <summary>
        /// The time the request takes.
        /// </summary>
        public TimeSpan ElapsedTime { get; set; }

        /// <summary>
        /// Add a result to the results storage.
        /// </summary>
        internal void AddResult(String mapping, IEnumerable<String> result) {
            throw new NotImplementedException();
        }

    // Method
        /// <summary>
        /// Scrape html with jQuery selectors.
        /// </summary>
        /// <param name="html">The html to scrape.</param>
        /// <param name="selectorMappings">A collection of jquery selectors.</param>
        /// <returns>The result of that scape.</returns>
        public static jScrape Scrape(String html, IEnumerable<String> selectorMappings)
        {
            if (String.IsNullOrWhiteSpace(html))
                throw new ArgumentNullException("html");

            HTMLDocument document = HtmlDocumentHelper.LoadDocument(html);
            return jScrape.Scrape(document, selectorMappings);
        }

        /// <summary>
        /// Scrape html with jQuery selectors.
        /// </summary>
        /// <param name="source">The uriform resource identifier of the page to scrape.</param>
        /// <param name="selectorMappings">A collection of jquery selectors.</param>
        /// <returns>The result of that scape.</return>
        public static jScrape Scrape(Uri source, IEnumerable<String> selectorMappings)
        {
            if(source == null)
                throw new ArgumentNullException("source");

            HTMLDocument document = HtmlDocumentHelper.DownloadDocument(source, null, true);
            return jScrape.Scrape(document, selectorMappings);
        }

#warning TODO: Refactor Point do not use a static counter - add the document to the scrape object, then you will only load default items the document once. 
		// The scrape methods will become instance methods, consider making the mappings object a dictionary which will help internally for the id issue,
		// It would also help with retrieving the mapping results. Consider making a copy of the document before mapping it.
		private static Int32 i = 0;
        /// <summary>
        /// Scrape html with jQuery selectors.
        /// </summary>
        /// <param name="document">The <see cref="mshtml.HTMLDocument"/> to scrape.</param>
        /// <param name="selectorMappings">A collection of jquery selectors.</param>
        /// <returns>The result of that scape.</return>
        public static jScrape Scrape(HTMLDocument document, IEnumerable<String> selectorMappings)
        {
            // Check arguments
            if (selectorMappings == null || selectorMappings.Count() == 0)
                throw new ArgumentNullException("selectorMappings");

            if(document == null)
                throw new ArgumentNullException("document");

            // Load Default Items.
            jScrape.LoadDefaultItems(document);

            // Setup jQuery scrape  
			i++;
			Int32 startIndex = i;
            foreach (String selector in selectorMappings)
            {               
                String safeSearchResultJavascript =
                    String.Format("setupScrape('{0}', '{1}' );", 
                                  selector, 
                                  "mapping-" + i.ToString());

                document.LoadScript(safeSearchResultJavascript);
                i++;
            } 
            
            // Harvest the results
            jScrape scrape = new jScrape();
            var results = new Dictionary<String, IEnumerable<ScrapeResult>>();
			i = startIndex;
            foreach (String selector in selectorMappings) 
            {
                IEnumerable<IHTMLElement> scrapeResultElement =
                    jScrape.HarvestResults(document, "mapping-" + i.ToString());

                if (scrapeResultElement != null && scrapeResultElement.Count() > 0)
                {
                    IEnumerable<ScrapeResult> scrapeResult = scrapeResultElement.Select(r => new ScrapeResult(r));
                    results.Add(selector, scrapeResult);
                }
                else
                {
                    ConsoleLogger.LogError("  mapping-" + i.ToString() + " Not Found!!!");
                }

                i++;
            }
            if (results.Count() > 0)
            {
                scrape.Results = results;
            }

			i = startIndex;
            return scrape;
        }

		/// <summary>
		/// Scrape a single result.
		/// </summary>
		/// <param name="document">The <see cref="mshtml.HTMLDocument"/> to scrape.</param>
		/// <param name="selector">The selector to scrape with.</param>
		/// <returns>A collection of scrape results.</returns>
		public static IEnumerable<ScrapeResult> ScrapeSingle(HTMLDocument document, String selector)
		{
			jScrape scrape = Scrape(document, new string[] { selector });

			if (scrape.Results != null && scrape.Results.Any())
			{
				return scrape.Results.First().Value;
			}
			return null;			
		}

        /// <summary>
        /// Collect results from the HTML document.
        /// </summary>
        /// <param name="document">The <see cref="mshtml.HTMLDocument"/> that is being parsed.</param>
        /// <param name="mappingName">the mapping to harvest.</param>
        /// <returns></returns>
        private static IEnumerable<IHTMLElement> HarvestResults(HTMLDocument document, string mappingName)
        {
            IHTMLElement searchResultElements = document.getElementById(mappingName);
            if (searchResultElements == null)
            {
                return null;
            }

            List<IHTMLElement> results = new List<IHTMLElement>();            

            // Loop through the list that was created to contain the mapped html
            foreach (IHTMLElement item in searchResultElements.children)
            {
                foreach (IHTMLElement resultElement in item.children)
                {
                    results.Add((IHTMLElement)resultElement);
                }
            }

            string debugDocument = document.documentElement.outerHTML;

            return results;
        }

        ///// <summary>
        ///// The results from mapping a search results page.
        ///// </summary>
        ///// <param name="mapping">The jQuery selector used to map a web page.</param>
        ///// <returns>Return a collection of search results.</returns>
        //public IEnumerable<String> GetResults(String mapping)
        //{
        //    throw new NotImplementedException();
        //}

        ///// <summary>
        ///// The results from mapping a search results page.
        ///// </summary>
        ///// <param name="mapping">The jQuery selector used to map a web page.</param>
        ///// <returns>Return a collection of search results.</returns>
        //public IEnumerable<XElement> GetXResults(String mapping)
        //{
        //    throw new NotImplementedException();
        //}

        /// <summary>
        /// Load the default javascript and CSS pages into the document so we can execute and troubleshoot.
        /// </summary>
        /// <param name="document">The current working document.</param>
        private static void LoadDefaultItems(HTMLDocument document)
        {
            document.ReferenceJavaScriptFile(new Uri("http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"));

            /*
            String applicationDataFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData);
            String assemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            String configDirectory = ConfigurationManager.AppSettings[HtmlHelperKey];


            //String localResultsFile = assemblyDirectory + jScrape.ResultJavascriptRelitivePath;
            //String applicationDataResultsFile = applicationDataFolder + jScrape.ResultJavascriptRelitivePath;		

            String scriptLocation;
            String resultStyleLocation;

            if (File.Exists(configDirectory + jScrape.ResultJavascriptRelitivePath))
			{
                scriptLocation = configDirectory + jScrape.ResultJavascriptRelitivePath;
                resultStyleLocation = configDirectory + jScrape.ResultStyleSheetRelitivePath;
			}
            else if (File.Exists(assemblyDirectory + jScrape.ResultJavascriptRelitivePath))
            {
                scriptLocation = assemblyDirectory + jScrape.ResultJavascriptRelitivePath;
                resultStyleLocation = assemblyDirectory + jScrape.ResultStyleSheetRelitivePath;
            }
            else if (File.Exists(applicationDataFolder + jScrape.ResultJavascriptRelitivePath))
			{
                scriptLocation = applicationDataFolder + jScrape.ResultJavascriptRelitivePath;
                resultStyleLocation = applicationDataFolder + jScrape.ResultStyleSheetRelitivePath;
			}            
            else
			{
				throw new InvalidOperationException(String.Format("Could not find Results.js or ResultStyless.css: \r\n{0}\r\n{1}",  applicationDataFolder, assemblyDirectory));
			}
            */

            document.LoadScript(ReadEmbeddedJavascript());
            // document.ReferanceStyleSheet(new Uri(resultStyleLocation));
        }

        /// <summary>
        /// Create some sort of quick debugging message.
        /// </summary>
        /// <returns>A some reference data.</returns>
        public override string ToString()
        {
            try
            {
                IEnumerable<String> itemString = 
                    this.Results.Select(
                        r => r.Value.First() + " ... " + r.Value.Last());

                return itemString.First();
            }
            catch (Exception)
            {
                return base.ToString();
            }            
        }

        public static String ReadEmbeddedJavascript() 
        {
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("jTest.HtmlHelpers.EmbeddResults.js"))
            using (StreamReader reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }

    }    
}
